package org.xutils.db.impl;


import net.sqlcipher.Cursor;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteStatement;

import org.xutils.db.DbManager;
import org.xutils.db.Selector;
import org.xutils.db.ex.DbException;
import org.xutils.db.log.Log;
import org.xutils.db.model.DDLTableModel;
import org.xutils.db.model.KeyValue;
import org.xutils.db.sqlite.SqlInfo;
import org.xutils.db.sqlite.SqlInfoBuilder;
import org.xutils.db.sqlite.WhereBuilder;
import org.xutils.db.table.ColumnEntity;
import org.xutils.db.table.DbBase;
import org.xutils.db.table.DbModel;
import org.xutils.db.table.TableEntity;
import org.xutils.db.util.CursorUtils;
import org.xutils.db.util.IOUtil;
import org.xutils.db.xDb;

import java.io.File;
import java.io.IOException;
import java.nio.file.OpenOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;

public final class DbManagerImpl extends DbBase {

    /**
     * key: dbName
     */
    private final static HashMap<DaoConfig, DbManagerImpl> DAO_MAP = new HashMap<DaoConfig, DbManagerImpl>();

    private SQLiteDatabase database;
    private DaoConfig daoConfig;
    private boolean allowTransaction;
    private boolean allowErrorEndTransaction;

    private DbManagerImpl(DaoConfig config) throws DbException {
        if (config == null) {
            throw new IllegalArgumentException("daoConfig may not be null");
        }

        this.daoConfig = config;
        this.allowTransaction = config.isAllowTransaction();
        this.allowErrorEndTransaction = config.isAllowErrorEndTransaction();
        try {
            this.database = openOrCreateDatabase(config);
            DbOpenListener dbOpenListener = config.getDbOpenListener();
            if (dbOpenListener != null) {
                dbOpenListener.onDbOpened(this);
            }
        } catch (DbException ex) {
            IOUtil.closeSafe(this.database);
            database.close();
            throw ex;
        } catch (Throwable ex) {
            IOUtil.closeSafe(this.database);
            throw new DbException(ex.getMessage(), ex);
        }
    }

    public synchronized static DbManager getInstance(DaoConfig daoConfig) throws DbException {

        if (daoConfig == null) {//使用默认配置
            daoConfig = new DaoConfig();
        }

        DbManagerImpl dao = DAO_MAP.get(daoConfig);
        if (dao == null) {
            dao = new DbManagerImpl(daoConfig);
            DAO_MAP.put(daoConfig, dao);
        } else {
            dao.daoConfig = daoConfig;
        }

        // update the database if needed
        SQLiteDatabase database = dao.database;
        int oldVersion = database.getVersion();
        int newVersion = daoConfig.getDbVersion();
        if (oldVersion != newVersion) {
            if (oldVersion != 0) {
                DbUpgradeListener upgradeListener = daoConfig.getDbUpgradeListener();
                if (upgradeListener != null) {
                    upgradeListener.onUpgrade(dao, oldVersion, newVersion);
                } else {
                    dao.dropDb();
                }
            }
            database.setVersion(newVersion);
        }

        return dao;
    }

    @Override
    public SQLiteDatabase getDatabase() {
        return database;
    }

    @Override
    public DaoConfig getDaoConfig() {
        return daoConfig;
    }

    //*********************************************** operations ********************************************************

    @Override
    public void saveOrUpdate(Object entity) throws DbException {
        try {
            beginTransaction();

            if (entity instanceof List) {
                List<?> entities = (List<?>) entity;
                if (entities.isEmpty()) return;
                TableEntity<?> table = this.getTable(entities.get(0).getClass());
                table.createTableIfNotExists();
                for (Object item : entities) {
                    saveOrUpdateWithoutTransaction(table, item);
                }
            } else {
                TableEntity<?> table = this.getTable(entity.getClass());
                table.createTableIfNotExists();
                saveOrUpdateWithoutTransaction(table, entity);
            }

            setTransactionSuccessful();
        } finally {
            endTransaction();
        }
    }

    @Override
    public void replace(Object entity) throws DbException {
        try {
            beginTransaction();

            if (entity instanceof List) {
                List<?> entities = (List<?>) entity;
                if (entities.isEmpty()) return;
                TableEntity<?> table = this.getTable(entities.get(0).getClass());
                table.createTableIfNotExists();
                for (Object item : entities) {
                    execNonQuery(SqlInfoBuilder.buildReplaceSqlInfo(table, item));
                }
            } else {
                TableEntity<?> table = this.getTable(entity.getClass());
                table.createTableIfNotExists();
                execNonQuery(SqlInfoBuilder.buildReplaceSqlInfo(table, entity));
            }

            setTransactionSuccessful();
        } finally {
            endTransaction();
        }
    }

    @Override
    public void save(Object entity) throws DbException {
        try {
            beginTransaction();

            if (entity instanceof List) {
                List<?> entities = (List<?>) entity;
                if (entities.isEmpty()) {
                    return;
                }
                TableEntity<?> table = this.getTable(entities.get(0).getClass());
                table.createTableIfNotExists();
                for (Object item : entities) {
                    execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, item));
                }
            } else {
                TableEntity<?> table = this.getTable(entity.getClass());
                table.createTableIfNotExists();
                execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, entity));
            }

            setTransactionSuccessful();
        } finally {
            endTransaction();
        }
    }

    @Override
    public void saveNoTransaction(Object entity) throws DbException {
        try {
            if (entity instanceof List) {
                List<?> entities = (List<?>) entity;
                if (entities.isEmpty()) {
                    return;
                }
                TableEntity<?> table = this.getTable(entities.get(0).getClass());
                table.createTableIfNotExists();
                for (Object item : entities) {
                    execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, item));
                }
            } else {
                TableEntity<?> table = this.getTable(entity.getClass());
                table.createTableIfNotExists();
                execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, entity));
            }

        } catch (Exception ignore) {
            endAutoTransaction();
        }
    }

    @Override
    public boolean saveBindingId(Object entity) throws DbException {
        boolean result = false;
        try {
            beginTransaction();

            if (entity instanceof List) {
                List<?> entities = (List<?>) entity;
                if (entities.isEmpty()) return false;
                TableEntity<?> table = this.getTable(entities.get(0).getClass());
                table.createTableIfNotExists();
                for (Object item : entities) {
                    if (!saveBindingIdWithoutTransaction(table, item)) {
                        throw new DbException("saveBindingId error, transaction will not commit!");
                    }
                }
            } else {
                TableEntity<?> table = this.getTable(entity.getClass());
                table.createTableIfNotExists();
                result = saveBindingIdWithoutTransaction(table, entity);
            }

            setTransactionSuccessful();
        } finally {
            endTransaction();
        }
        return result;
    }

    @Override
    public void deleteById(Class<?> entityType, Object idValue) throws DbException {
        TableEntity<?> table = this.getTable(entityType);
        if (!table.tableIsExists()) return;
        try {
            beginTransaction();

            execNonQuery(SqlInfoBuilder.buildDeleteSqlInfoById(table, idValue));

            setTransactionSuccessful();
        } finally {
            endTransaction();
        }
    }

    @Override
    public void delete(Object entity) throws DbException {
        try {
            beginTransaction();

            if (entity instanceof List) {
                List<?> entities = (List<?>) entity;
                if (entities.isEmpty()) {
                    return;
                }
                TableEntity<?> table = this.getTable(entities.get(0).getClass());
                if (!table.tableIsExists()) {
                    return;
                }
                for (Object item : entities) {
                    execNonQuery(SqlInfoBuilder.buildDeleteSqlInfo(table, item));
                }
            } else {
                TableEntity<?> table = this.getTable(entity.getClass());
                if (!table.tableIsExists()) return;
                execNonQuery(SqlInfoBuilder.buildDeleteSqlInfo(table, entity));
            }

            setTransactionSuccessful();
        } finally {
            endTransaction();
        }
    }

    @Override
    public void delete(Class<?> entityType) throws DbException {
        delete(entityType, null);
    }

    @Override
    public int delete(Class<?> entityType, WhereBuilder whereBuilder) throws DbException {
        TableEntity<?> table = this.getTable(entityType);
        if (!table.tableIsExists()) return 0;
        int result = 0;
        try {
            beginTransaction();

            result = executeUpdateDelete(SqlInfoBuilder.buildDeleteSqlInfo(table, whereBuilder));

            setTransactionSuccessful();
        } finally {
            endTransaction();
        }
        return result;
    }

    @Override
    public void update(Object entity, String... updateColumnNames) throws DbException {
        try {
            beginTransaction();

            if (entity instanceof List) {
                List<?> entities = (List<?>) entity;
                if (entities.isEmpty()) return;
                TableEntity<?> table = this.getTable(entities.get(0).getClass());
                if (!table.tableIsExists()) return;
                for (Object item : entities) {
                    execNonQuery(SqlInfoBuilder.buildUpdateSqlInfo(table, item, updateColumnNames));
                }
            } else {
                TableEntity<?> table = this.getTable(entity.getClass());
                if (!table.tableIsExists()) return;
                execNonQuery(SqlInfoBuilder.buildUpdateSqlInfo(table, entity, updateColumnNames));
            }

            setTransactionSuccessful();
        } finally {
            endTransaction();
        }
    }

    @Override
    public void updateNoTransaction(Object entity, String... updateColumnNames) throws DbException {
        try {
            if (entity instanceof List) {
                List<?> entities = (List<?>) entity;
                if (entities.isEmpty()) return;
                TableEntity<?> table = this.getTable(entities.get(0).getClass());
                if (!table.tableIsExists()) return;
                for (Object item : entities) {
                    execNonQuery(SqlInfoBuilder.buildUpdateSqlInfo(table, item, updateColumnNames));
                }
            } else {
                TableEntity<?> table = this.getTable(entity.getClass());
                if (!table.tableIsExists()) return;
                execNonQuery(SqlInfoBuilder.buildUpdateSqlInfo(table, entity, updateColumnNames));
            }
        } catch (Exception ignore) {
            endTransaction();
        }
    }

    @Override
    public int updateNoTransaction(Class<?> entityType, WhereBuilder whereBuilder, KeyValue... nameValuePairs) throws DbException {
        TableEntity<?> table = this.getTable(entityType);
        if (!table.tableIsExists()) return 0;
        int result = 0;
        try {
            result = executeUpdateDelete(SqlInfoBuilder.buildUpdateSqlInfo(table, whereBuilder, nameValuePairs));
        } catch (Exception ignore) {
            endTransaction();
        }

        return result;
    }

    @Override
    public int update(Class<?> entityType, WhereBuilder whereBuilder, KeyValue... nameValuePairs) throws DbException {
        TableEntity<?> table = this.getTable(entityType);
        if (!table.tableIsExists()) return 0;

        int result = 0;
        try {
            beginTransaction();

            result = executeUpdateDelete(SqlInfoBuilder.buildUpdateSqlInfo(table, whereBuilder, nameValuePairs));

            setTransactionSuccessful();
        } finally {
            endTransaction();
        }

        return result;
    }

    @Override
    public <T> T findById(Class<T> entityType, Object idValue) throws DbException {
        TableEntity<T> table = this.getTable(entityType);
        if (!table.tableIsExists()) return null;

        Selector<T> selector = Selector.from(table).where(table.getId().getName(), "=", idValue);
        String sql = selector.limit(1).toString();
        Cursor cursor = execQuery(sql);
        if (cursor != null) {
            try {
                if (cursor.moveToNext()) {
                    return CursorUtils.getEntity(table, cursor);
                }
            } catch (Throwable e) {
                throw new DbException(e);
            } finally {
                IOUtil.closeQuietly(cursor);
            }
        }
        return null;
    }

    @Override
    public <T> T findByPrimaryKey(Class<T> entityType, String pk, Object idValue) throws DbException {
        TableEntity<T> table = this.getTable(entityType);
        if (!table.tableIsExists()) return null;

        Selector<T> selector = Selector.from(table).where(pk, "=", idValue);
        String sql = selector.limit(1).toString();
        Cursor cursor = execQuery(sql);
        if (cursor != null) {
            try {
                if (cursor.moveToNext()) {
                    return CursorUtils.getEntity(table, cursor);
                }
            } catch (Throwable e) {
                throw new DbException(e);
            } finally {
                IOUtil.closeQuietly(cursor);
            }
        }
        return null;
    }

    @Override
    public <T> T findFirst(Class<T> entityType) throws DbException {
        return this.selector(entityType).findFirst();
    }

    @Override
    public <T> List<T> findAll(Class<T> entityType) throws DbException {
        return this.selector(entityType).findAll();
    }

    @Override
    public <T> Selector<T> selector(Class<T> entityType) throws DbException {
        return Selector.from(this.getTable(entityType));
    }

    @Override
    public DbModel findDbModelFirst(SqlInfo sqlInfo) throws DbException {
        Cursor cursor = execQuery(sqlInfo);
        if (cursor != null) {
            try {
                if (cursor.moveToNext()) {
                    return CursorUtils.getDbModel(cursor);
                }
            } catch (Throwable e) {
                throw new DbException(e);
            } finally {
                IOUtil.closeQuietly(cursor);
            }
        }
        return null;
    }

    @Override
    public List<DbModel> findDbModelAll(SqlInfo sqlInfo) throws DbException {
        List<DbModel> dbModelList = new ArrayList<DbModel>();

        Cursor cursor = execQuery(sqlInfo);
        if (cursor != null) {
            try {
                while (cursor.moveToNext()) {
                    dbModelList.add(CursorUtils.getDbModel(cursor));
                }
            } catch (Throwable e) {
                throw new DbException(e);
            } finally {
                IOUtil.closeQuietly(cursor);
            }
        }
        return dbModelList;
    }

    @Override
    public Optional<DDLTableModel> getTable(String name) throws DbException {
        return getTable("table", name);
    }

    @Override
    public Optional<DDLTableModel> getTable(String type, String name) throws DbException {
        Optional<DDLTableModel> optional = Optional.empty();

        Cursor cursor = getDatabase().rawQuery("SELECT type, name, tbl_name, rootpage, sql FROM sqlite_master WHERE " +
                        "type=? " +
                        "AND " +
                        "name=?",
                new String[]{type, name});


        if (cursor != null && cursor.getCount() > 0) {
            boolean moveToFirst = cursor.moveToFirst();
            if (moveToFirst) {
                String typeTmp = cursor.getString(cursor.getColumnIndex("type"));
                String nameTmp = cursor.getString(cursor.getColumnIndex("name"));
                String tbl_name = cursor.getString(cursor.getColumnIndex("tbl_name"));
                int rootpage = cursor.getInt(cursor.getColumnIndex("rootpage"));
                String sql = cursor.getString(cursor.getColumnIndex("sql"));
                DDLTableModel dtm = new DDLTableModel(typeTmp, nameTmp, rootpage, tbl_name, sql);
                optional = Optional.of(dtm);
            }
        }
        return optional;
    }

    /**
     * 获取所有的表
     *
     * @return
     * @throws DbException
     */
    @Override
    public Optional<List<DDLTableModel>> getAllTables() throws DbException {
        Optional<List<DDLTableModel>> optional = Optional.empty();

        Cursor cursor = getDatabase().rawQuery("SELECT type, name, tbl_name, rootpage, sql FROM sqlite_master WHERE " +
                        "type='table'",
                null);


        if (cursor != null && cursor.getCount() > 0) {
            boolean moveToFirst = cursor.moveToFirst();
            Log.getInstance().dx("zzh: moveToFirst" + moveToFirst);
            List<DDLTableModel> data = new ArrayList<>();
            while (cursor.moveToNext()) {
                String type = cursor.getString(cursor.getColumnIndex("type"));
                String name = cursor.getString(cursor.getColumnIndex("name"));
                String tbl_name = cursor.getString(cursor.getColumnIndex("tbl_name"));
                int rootpage = cursor.getInt(cursor.getColumnIndex("rootpage"));
                String sql = cursor.getString(cursor.getColumnIndex("sql"));
                DDLTableModel dtm = new DDLTableModel(type, name, rootpage, tbl_name, sql);
                data.add(dtm);
            }
            optional = Optional.of(data);

        }
        return optional;
    }

    /**
     * 获取
     *
     * @return
     * @throws DbException
     */
    @Override
    public Optional<List<DDLTableModel>> getTables() throws DbException {


        Optional<List<DDLTableModel>> optional = Optional.empty();

        Cursor cursor = getDatabase().rawQuery("SELECT type, name, tbl_name, rootpage, sql FROM sqlite_master",
                null);

        if (cursor != null && cursor.getCount() > 0) {
            boolean moveToFirst = cursor.moveToFirst();
            Log.getInstance().dx("zzh: moveToFirst" + moveToFirst);
            List<DDLTableModel> data = new ArrayList<>();
            while (cursor.moveToNext()) {
                String type = cursor.getString(cursor.getColumnIndex("type"));
                String name = cursor.getString(cursor.getColumnIndex("name"));
                String tbl_name = cursor.getString(cursor.getColumnIndex("tbl_name"));
                int rootpage = cursor.getInt(cursor.getColumnIndex("rootpage"));
                String sql = cursor.getString(cursor.getColumnIndex("sql"));
                DDLTableModel dtm = new DDLTableModel(type, name, rootpage, tbl_name, sql);
                data.add(dtm);
            }
            optional = Optional.of(data);

        }
        return optional;
    }

    //******************************************** config ******************************************************

    private SQLiteDatabase openOrCreateDatabase(DaoConfig config) {
        SQLiteDatabase result = null;

        File dbDir = config.getDbDir();

        if (dbDir == null) {
            dbDir = xDb.getInstance().getContext().getDatabasePath(config.getDbName());
            result = SQLiteDatabase.openOrCreateDatabase(dbDir, config.getDbEncrypt(), null);
        } else {
            if (!dbDir.exists()) {
                dbDir.mkdirs();
            }
            result = SQLiteDatabase.openOrCreateDatabase(new File(dbDir, config.getDbName()), config.getDbEncrypt(), null);
        }

        return result;
    }

    /***************************** private operations with out transaction *****************************/
    private void saveOrUpdateWithoutTransaction(TableEntity<?> table, Object entity) throws DbException {
        ColumnEntity id = table.getId();
        if (id.isAutoId()) {
            Log.getInstance().e(new DbException("自动生成的ID"));
            Object value = id.getColumnValue(entity);
            if (value != null) {
                Log.getInstance().e(new DbException("更新操作，ID 不为空：" + value + "，构建更新SQL"));
                execNonQuery(SqlInfoBuilder.buildUpdateSqlInfo(table, entity));
            } else {
                Log.getInstance().e(new DbException("插入操作，ID 为空，构建插入SQL"));
                saveBindingIdWithoutTransaction(table, entity);
            }
        } else {
            Log.getInstance().e(new DbException("不自动生成的ID"));
            execNonQuery(SqlInfoBuilder.buildReplaceSqlInfo(table, entity));
        }

    }

    private boolean saveBindingIdWithoutTransaction(TableEntity<?> table, Object entity) throws DbException {
        ColumnEntity id = table.getId();
        if (id.isAutoId()) {
            //是否是需要自动生成ID，是则需要判断是number类型的主键自动生成，还是字符串类型的主键
            if (id.isAutoNumberId()) {
                execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, entity));
                long idValue = getLastAutoIncrementId(table.getName());
                if (idValue == -1) {
                    return false;
                }
                id.setAutoIdValue(entity, idValue);
            } else {
                //字符串，主键如何传入
                String pkId = xDb.getInstance().generate();
                execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, entity, pkId));
                id.setAutoIdValue(entity, pkId);
            }
        } else {
            execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, entity));
        }
        return true;
    }

    //************************************************ tools ***********************************
    private long getLastAutoIncrementId(String tableName) throws DbException {
        long id = -1;
        Cursor cursor = execQuery("SELECT seq FROM sqlite_sequence WHERE name='" + tableName + "' LIMIT 1");
        if (cursor != null) {
            try {
                if (cursor.moveToNext()) {
                    id = cursor.getLong(0);
                }
            } catch (Throwable e) {
                id = -1;
            } finally {
                IOUtil.closeQuietly(cursor);
            }
        }
        return id;
    }

    /**
     * 关闭数据库.
     * 同一个库的是单实例的, 尽量不要调用这个方法, 会自动释放.
     */
    @Override
    public void close() throws IOException {
        if (DAO_MAP.containsKey(daoConfig)) {
            DAO_MAP.remove(daoConfig);
            this.database.close();
        }
    }

    ///////////////////////////////////// exec sql /////////////////////////////////////////////////////

    public void beginTransaction() {
        if (allowTransaction) {
            if (database.isWriteAheadLoggingEnabled()) {
                database.beginTransactionNonExclusive();
            } else {
                database.beginTransaction();
            }
        }
    }

    public void setTransactionSuccessful() {
        if (allowTransaction) {
            database.setTransactionSuccessful();
        }
    }

    public void endTransaction() {
        if (allowTransaction) {
            database.endTransaction();
        }
    }

    @Override
    public void beginAutoTransaction() {
        if (database.isWriteAheadLoggingEnabled()) {
            database.beginTransactionNonExclusive();
        } else {
            database.beginTransaction();
        }
    }

    @Override
    public void setAutoTransactionSuccessful() {
        if (allowErrorEndTransaction) {
            database.setTransactionSuccessful();
        }
    }

    @Override
    public void endAutoTransaction() {
        if (allowErrorEndTransaction) {
            database.endTransaction();
        }
    }


    @Override
    public int executeUpdateDelete(SqlInfo sqlInfo) throws DbException {
        SQLiteStatement statement = null;
        try {
            statement = sqlInfo.buildStatement(database);
            return statement.executeUpdateDelete();
        } catch (Throwable e) {
            throw new DbException(e);
        } finally {
            if (statement != null) {
                try {
                    statement.releaseReference();
                } catch (Throwable ex) {
                    Log.getInstance().e(ex);
                }
            }
        }
    }

    @Override
    public int executeUpdateDelete(String sql) throws DbException {
        SQLiteStatement statement = null;
        try {
            statement = database.compileStatement(sql);
            return statement.executeUpdateDelete();
        } catch (Throwable e) {
            throw new DbException(e);
        } finally {
            if (statement != null) {
                try {
                    statement.releaseReference();
                } catch (Throwable ex) {
                    Log.getInstance().e(ex);
                }
            }
        }
    }

    @Override
    public void execNonQuery(SqlInfo sqlInfo) throws DbException {
        SQLiteStatement statement = null;
        try {
            statement = sqlInfo.buildStatement(database);
            statement.execute();
        } catch (Throwable e) {
            throw new DbException(e);
        } finally {
            if (statement != null) {
                try {
                    statement.releaseReference();
                } catch (Throwable ex) {
                    Log.getInstance().e(ex);
                }
            }
        }
    }

    @Override
    public void execNonQuery(String sql) throws DbException {
        try {
            database.execSQL(sql);
        } catch (Throwable e) {
            throw new DbException(e);
        }
    }

    @Override
    public Cursor execQuery(SqlInfo sqlInfo) throws DbException {
        try {
            return database.rawQuery(sqlInfo.getSql(), sqlInfo.getBindArgsAsStrArray());
        } catch (Throwable e) {
            throw new DbException(e);
        }
    }

    @Override
    public Cursor execQuery(String sql) throws DbException {
        try {
            Log.getInstance().d(new Exception("---执行语句：" + sql));
            return database.rawQuery(sql, null);
        } catch (Throwable e) {
            throw new DbException(e);
        }
    }

}
